using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;


namespace _2DGame
{
    /// <summary>
    /// This is a game component that implements IUpdateable.
    /// </summary>
    public class Map : CollisionObject
    {
        public class CollisionInfo
        {
            public enum CollisionType { Empty, Destroyable, Safe, Solid };

            public CollisionType type;
            public Vector2 normal;
        }

        String collisionFilename;
        CollisionInfo[] collisionMap;

        String fillerFilename;
        Texture2D fillerTexture;

        List<Vector2> spawnPoints = new List<Vector2>();

        bool drawCollisionMap = false;

        public Map(String fromFilename, String fromCollisionFilename, String fromFillerFilename) :
            base(fromFilename, SpriteBlendMode.AlphaBlend)
        {
            collisionFilename = fromCollisionFilename;

            Texture2D collisionTexture;

            if (Resources.TextureBank.TryGetValue(collisionFilename, out collisionTexture) == true)
            {
                if (Texture != null)
                {
                    Color[] collisionMap = new Color[collisionTexture.Width * collisionTexture.Height];
                    collisionTexture.GetData<Color>(collisionMap);

                    ProcessCollisionMap(collisionTexture.Width, collisionTexture.Height, collisionMap);
                }
            }
            else
            {
                // Textures will be loaded into the bank during World.LoadContent
                // And we'll make our textureData then
                Resources.TextureBank.Add(collisionFilename, null);
            }

            fillerFilename = fromFillerFilename;

            if (Resources.TextureBank.TryGetValue(fillerFilename, out fillerTexture) == false)
            {
                // Textures will be loaded into the bank during World.LoadContent
                // And we'll make our textureData then
                Resources.TextureBank.Add(fillerFilename, null);
            }
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        public override void LoadContent()
        {
            base.LoadContent();

            Texture2D collisionTexture = Resources.TextureBank[collisionFilename];
            Color[] collisionMap = new Color[collisionTexture.Width * collisionTexture.Height];
            collisionTexture.GetData<Color>(collisionMap);

            ProcessCollisionMap(collisionTexture.Width, collisionTexture.Height, collisionMap);

            fillerTexture = Resources.TextureBank[fillerFilename];
        }

        void ProcessCollisionMap(int width, int height, Color[] textureData)
        {
            Texture2D collisionTexture = Resources.TextureBank[collisionFilename];
            collisionMap = new CollisionInfo[width * height];

            for (int y = 0; y < height; ++y)
            {
                for (int x = 0; x < width; ++x)
                {
                    Color currentColor = textureData[(y * width) + x];
                    collisionMap[(y * width) + x] = new CollisionInfo();

                    if (currentColor == Color.Red)
                    {
                        collisionMap[(y * width) + x].type = CollisionInfo.CollisionType.Solid;
                    }
                    else if (currentColor == Color.Lime)
                    {
                        collisionMap[(y * width) + x].type = CollisionInfo.CollisionType.Destroyable;
                    }
                    else if (currentColor == Color.Blue)
                    {
                        collisionMap[(y * width) + x].type = CollisionInfo.CollisionType.Safe;
                    }
                    else if (currentColor == Color.Yellow)
                    {
                        spawnPoints.Add(new Vector2(x, y));
                    }
                    else if (currentColor == Color.Cyan)
                    {
                        Resources.World.AddPickupSpawnPoint(new Vector2(x, y));
                    }
                    else
                    {
                        collisionMap[(y * width) + x].type = CollisionInfo.CollisionType.Empty;
                    }
                }
            }
        }

        public Vector2 GetRandomSpawnPoint(float yOffset)
        {
            Vector2 spawnPoint = spawnPoints[Resources.Rand.Next(spawnPoints.Count)];
            spawnPoint.Y -= yOffset;

            return spawnPoint;
        }

        public bool GetClearSpawnPoint(float yOffset, float clearRadius, out Vector2 outPoint)
        {
            List<Vector2> validSpawnPoints = spawnPoints;

            foreach(Vector2 point in spawnPoints)
            {
                Vector2 spawnPoint = point;
                spawnPoint.Y -= yOffset;

                if(Resources.World.ObjectNear(spawnPoint, clearRadius) != null)
                    validSpawnPoints.Remove(spawnPoint);
            }

            if (validSpawnPoints.Count > 0)
            {
                outPoint = validSpawnPoints[Resources.Rand.Next(validSpawnPoints.Count)];
                return true;
            }
            else
            {
                outPoint = Vector2.Zero;
                return false;
            }
        }

        /// <summary>
        /// Allows the game component to update itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        public override void Update(GameTime gameTime)
        {
        }

        public override void Draw()
        {
            int scaledWidth = (int)(Resources.Device.Viewport.Width * (1.0f / Resources.World.DrawScale));
            int scaledHeight = (int)(Resources.Device.Viewport.Height * (1.0f / Resources.World.DrawScale));
            Rectangle sourceRect = new Rectangle();

            sourceRect.X = (int)Math.Round(Resources.World.CameraPosition.X) - (scaledWidth / 2);
            sourceRect.Y = (int)Math.Round(Resources.World.CameraPosition.Y) - (scaledHeight / 2);
            sourceRect.Width = scaledWidth;
            sourceRect.Height = scaledHeight;

            if (drawCollisionMap == false)
            {
                Resources.SpriteBatch.Draw(
                    Texture,
                    new Rectangle(0,
                        0,
                        Resources.Device.Viewport.Width,
                        Resources.Device.Viewport.Height),
                    sourceRect,
                    Color.White);
            }
            else
            {
                Resources.SpriteBatch.Draw(
                    Resources.TextureBank[collisionFilename],
                    new Rectangle(0,
                        0,
                        Resources.Device.Viewport.Width,
                        Resources.Device.Viewport.Height),
                    sourceRect,
                    Color.White);
            }

            // If we're drawing off the edge of the map, cover it up
            if (sourceRect.X < 0)
            {
                Rectangle fillInRectangle = new Rectangle();
                fillInRectangle.X = -sourceRect.X;
                fillInRectangle.Y = sourceRect.Y;
                fillInRectangle.Width = sourceRect.X;
                fillInRectangle.Height = scaledHeight;

                Resources.SpriteBatch.Draw(
                    fillerTexture,
                    new Rectangle(0,
                        0,
                        -(int)(sourceRect.X * Resources.World.DrawScale),
                        Resources.Device.Viewport.Height),
                    fillInRectangle,
                    Color.White);
            }
            if (sourceRect.Y < 0)
            {
                Rectangle fillInRectangle = new Rectangle();
                fillInRectangle.X = sourceRect.X;
                fillInRectangle.Y = -sourceRect.Y;
                fillInRectangle.Width = scaledWidth;
                fillInRectangle.Height = sourceRect.Y;
                
                Resources.SpriteBatch.Draw(
                    fillerTexture,
                    new Rectangle(0,
                        0,
                        Resources.Device.Viewport.Width,
                        -(int)(sourceRect.Y * Resources.World.DrawScale)),
                    fillInRectangle,
                    Color.White);
            }
            if ((sourceRect.X + scaledWidth) > Texture.Width)
            {
                Rectangle fillInRectangle = new Rectangle();
                fillInRectangle.X = 0;
                fillInRectangle.Y = sourceRect.Y;
                fillInRectangle.Width = sourceRect.X;
                fillInRectangle.Height = scaledHeight;

                Resources.SpriteBatch.Draw(
                    fillerTexture,
                    new Rectangle((int)((Texture.Width - sourceRect.X) * Resources.World.DrawScale),
                        0,
                        (int)(sourceRect.X * Resources.World.DrawScale),
                        Resources.Device.Viewport.Height),
                    fillInRectangle,
                    Color.White);
            }
            if ((sourceRect.Y + scaledHeight) > Texture.Height)
            {
                Rectangle fillInRectangle = new Rectangle();
                fillInRectangle.X = sourceRect.X;
                fillInRectangle.Y = 0;
                fillInRectangle.Width = scaledWidth;
                fillInRectangle.Height = sourceRect.Y;

                Resources.SpriteBatch.Draw(
                    fillerTexture,
                    new Rectangle(0,
                        (int)((Texture.Height - sourceRect.Y) * Resources.World.DrawScale),
                        Resources.Device.Viewport.Width,
                        (int)(sourceRect.Y * Resources.World.DrawScale)),
                    fillInRectangle,
                    Color.White);
            }
        }

        public void DestroyTerrain(Vector2 centre, float radius)
        {
            Rectangle terrainEffected = new Rectangle(
                (int)(centre.X - radius),
                (int)(centre.Y - radius),
                (int)radius * 2,
                (int)radius * 2);

            if (terrainEffected.X < 0)
            {
                // Subtract the amount X is beyond 0 from the width
                // as X is already negative we add.
                terrainEffected.Width += terrainEffected.X;
                terrainEffected.X = 0;
            }
            if (terrainEffected.X + terrainEffected.Width > Texture.Width)
            {
                // Clamp to the right edge of the map
                terrainEffected.Width = Texture.Width - terrainEffected.X;
            }
            if (terrainEffected.Y < 0)
            {
                // Subtract the amount Y is beyond 0 from the height
                // as Y is already negative we add.
                terrainEffected.Height += terrainEffected.Y;
                terrainEffected.Y = 0;
            }
            if (terrainEffected.Y + terrainEffected.Height > Texture.Height)
            {
                // Clamp to the bottom edge of the map
                terrainEffected.Height = Texture.Height - terrainEffected.Y;
            }
            float radiusSquared = radius * radius;

            List<Point> changedPoints = new List<Point>();

            for (int y = terrainEffected.Y; y < terrainEffected.Y + terrainEffected.Height; ++y)
            {
                for (int x = terrainEffected.X; x < terrainEffected.X + terrainEffected.Width; ++x)
                {
                    if (collisionMap[(y * Texture.Width) + x].type == CollisionInfo.CollisionType.Destroyable)
                    {
                        Vector2 dist = centre - new Vector2(x, y);

                        if (dist.LengthSquared() <= radiusSquared)
                        {
                            changedPoints.Add(new Point(x, y));
                        }
                    }
                }
            }

            if (changedPoints.Count > 0)
            {
                Rectangle changedRectangle = new Rectangle(changedPoints[0].X, changedPoints[0].Y, 1, 1);

                foreach (Point point in changedPoints)
                {
                    if (point.X < changedRectangle.X)
                    {
                        changedRectangle.Width += changedRectangle.X - point.X;
                        changedRectangle.X = point.X;
                    }
                    if (point.X >= changedRectangle.X + changedRectangle.Width)
                    {
                        changedRectangle.Width = (point.X - changedRectangle.X) + 1;
                    }
                    if (point.Y < changedRectangle.Y)
                    {
                        changedRectangle.Height += changedRectangle.Y - point.Y;
                        changedRectangle.Y = point.Y;
                    }
                    if (point.Y >= changedRectangle.Y + changedRectangle.Height)
                    {
                        changedRectangle.Height = (point.Y - changedRectangle.Y) + 1;
                    }
                }

                Color[] changedPixels = new Color[changedRectangle.Width * changedRectangle.Height];

                Texture.GetData(0, changedRectangle, changedPixels, 0, changedRectangle.Width * changedRectangle.Height);

                foreach (Point point in changedPoints)
                {
                    if (((point.X & 0x1) == 0) && ((point.Y & 0x1) == 0)) // If both X and Y are even, create a particle
                    {
                        float extraVelocityAngle = (float)(Resources.Rand.NextDouble() * Math.PI);
                        float extraVelocityMagnitude = ((float)Resources.Rand.NextDouble() - 0.5f) * 200.0f;
                        Vector2 particleVelocity = new Vector2((float)(Math.Cos(extraVelocityAngle) * extraVelocityMagnitude),
                                               (float)(Math.Sin(extraVelocityAngle) * extraVelocityMagnitude));

                        Resources.World.AddObject(new EffectObject(
                            "white_pixel",
                            SpriteBlendMode.AlphaBlend,
                            new Vector2(point.X, point.Y),
                            particleVelocity,
                            Resources.World.Gravity * 100.0f,
                            changedPixels[((point.Y - changedRectangle.Y) * changedRectangle.Width) + (point.X - changedRectangle.X)],
                            2.0f,
                            0.0f,
                            (float)Resources.Rand.NextDouble() + 0.5f));
                    }

                    collisionMap[point.X + point.Y * Texture.Width].type = CollisionInfo.CollisionType.Empty;
                    changedPixels[((point.Y - changedRectangle.Y) * changedRectangle.Width) + (point.X - changedRectangle.X)].A = 0;

                }

                Texture.SetData(0, changedRectangle, changedPixels, 0, changedRectangle.Width * changedRectangle.Height, SetDataOptions.None);
            }
        }

        public void DestroyTerrain(Matrix transform, int width, int height, Color[] colorData)
        {
            List<Point> changedPoints = new List<Point>();
            // When a point moves in A's local space, it moves in B's local space with a
            // fixed direction and distance proportional to the movement in A.
            // This algorithm steps through A one pixel at a time along A's X and Y axes
            // Calculate the analogous steps in B:
            Vector2 stepX = Vector2.TransformNormal(Vector2.UnitX, transform);
            Vector2 stepY = Vector2.TransformNormal(Vector2.UnitY, transform);

            // Calculate the top left corner of A in B's local space
            // This variable will be reused to keep track of the start of each row
            Vector2 yPosInB = Vector2.Transform(Vector2.Zero, transform);

            // For each row of pixels in A
            for (int yA = 0; yA < height; yA++)
            {
                // Start at the beginning of the row
                Vector2 posInB = yPosInB;

                // For each pixel in this row
                for (int xA = 0; xA < width; xA++)
                {
                    // Round to the nearest pixel
                    int xB = (int)Math.Round(posInB.X);
                    int yB = (int)Math.Round(posInB.Y);

                    // If the pixel lies within the bounds of B
                    if (0 <= xB && xB < Texture.Width &&
                        0 <= yB && yB < Texture.Height)
                    {
                        // Get the colors of the overlapping pixels
                        Color color = colorData[xA + yA * width];
                        CollisionInfo info = collisionMap[xB + yB * Texture.Width];

                        // If both pixels are not completely transparent,
                        if (color.A != 0 && info.type != CollisionInfo.CollisionType.Empty)
                        {
                            changedPoints.Add(new Point(xB, yB));
                        }
                    }

                    // Move to the next pixel in the row
                    posInB += stepX;
                }

                // Move to the next row
                yPosInB += stepY;
            }

            if (changedPoints.Count > 0)
            {
                Rectangle changedRectangle = new Rectangle(changedPoints[0].X, changedPoints[0].Y, 1, 1);

                foreach (Point point in changedPoints)
                {
                    if (point.X < changedRectangle.X)
                    {
                        changedRectangle.Width += changedRectangle.X - point.X;
                        changedRectangle.X = point.X;
                    }
                    if (point.X >= changedRectangle.X + changedRectangle.Width)
                    {
                        changedRectangle.Width = (point.X - changedRectangle.X) + 1;
                    }
                    if (point.Y < changedRectangle.Y)
                    {
                        changedRectangle.Height += changedRectangle.Y - point.Y;
                        changedRectangle.Y = point.Y;
                    }
                    if (point.Y >= changedRectangle.Y + changedRectangle.Height)
                    {
                        changedRectangle.Height = (point.Y - changedRectangle.Y) + 1;
                    }
                }

                Color[] changedPixels = new Color[changedRectangle.Width * changedRectangle.Height];

                Texture.GetData(0, changedRectangle, changedPixels, 0, changedRectangle.Width * changedRectangle.Height);

                foreach (Point point in changedPoints)
                {
                    if (((point.X & 0x1) == 0) && ((point.Y & 0x1) == 0)) // If both X and Y are even, create a particle
                    {
                        float extraVelocityAngle = (float)(Resources.Rand.NextDouble() * Math.PI);
                        float extraVelocityMagnitude = ((float)Resources.Rand.NextDouble() - 0.5f) * 200.0f;
                        Vector2 particleVelocity = new Vector2((float)(Math.Cos(extraVelocityAngle) * extraVelocityMagnitude),
                                               (float)(Math.Sin(extraVelocityAngle) * extraVelocityMagnitude));

                        Resources.World.AddObject(new EffectObject(
                            "white_pixel",
                            SpriteBlendMode.AlphaBlend,
                            new Vector2(point.X, point.Y),
                            particleVelocity,
                            Resources.World.Gravity * 100.0f,
                            changedPixels[((point.Y - changedRectangle.Y) * changedRectangle.Width) + (point.X - changedRectangle.X)],
                            2.0f,
                            0.0f,
                            (float)Resources.Rand.NextDouble() + 0.5f));
                    }

                    collisionMap[point.X + point.Y * Texture.Width].type = CollisionInfo.CollisionType.Empty;
                    changedPixels[((point.Y - changedRectangle.Y) * changedRectangle.Width) + (point.X - changedRectangle.X)].A = 0;
                }

                Texture.SetData(0, changedRectangle, changedPixels, 0, changedRectangle.Width * changedRectangle.Height, SetDataOptions.None);
            }
        }

        public bool IsColliding(GameObject otherObject, out List<Map.CollisionInfo> collisions)
        {
            Matrix otherTransform =
                Matrix.CreateTranslation(new Vector3(-otherObject.Texture.Width / 2, -otherObject.Texture.Height / 2, 0.0f)) *
                Matrix.CreateRotationZ(otherObject.Rotation) *
                Matrix.CreateTranslation(new Vector3(otherObject.Position, 0.0f));

            // Get the bounding rectangle of the person
            Rectangle mapRectangle = new Rectangle(0, 0, Texture.Width, Texture.Height);
            Rectangle objectTextureRectangle = otherObject.GetVisibleTextureRectangle();

            Rectangle objectRectangle = CalculateBoundingRectangle(
                         new Rectangle(0, 0, objectTextureRectangle.Width, objectTextureRectangle.Height),
                         otherTransform);

            // The other object will always be smaller than the map, so we choose that one
            return IntersectPixels(
                otherTransform, objectTextureRectangle, otherObject.Texture.Width, otherObject.TextureData,
                Texture.Width, Texture.Height, collisionMap,
                out collisions);
        }

        static bool IntersectPixels(
                            Matrix transform, Rectangle rectangleA, int strideA, Color[] colorData,
                            int widthB, int heightB, CollisionInfo[] collisionData,
                            out List<Map.CollisionInfo> collisions)
        {
            collisions = new List<CollisionInfo>();

            // When a point moves in A's local space, it moves in B's local space with a
            // fixed direction and distance proportional to the movement in A.
            // This algorithm steps through A one pixel at a time along A's X and Y axes
            // Calculate the analogous steps in B:
            Vector2 stepX = Vector2.TransformNormal(Vector2.UnitX, transform);
            Vector2 stepY = Vector2.TransformNormal(Vector2.UnitY, transform);

            // Calculate the top left corner of A in B's local space
            // This variable will be reused to keep track of the start of each row
            Vector2 yPosInB = Vector2.Transform(Vector2.Zero, transform);

            // For each row of pixels in A
            for (int yA = 0; yA < rectangleA.Height; yA++)
            {
                // Start at the beginning of the row
                Vector2 posInB = yPosInB;

                // For each pixel in this row
                for (int xA = 0; xA < rectangleA.Width; xA++)
                {
                    // Round to the nearest pixel
                    int xB = (int)Math.Round(posInB.X);
                    int yB = (int)Math.Round(posInB.Y);

                    // If the pixel lies within the bounds of B
                    if (0 <= xB && xB < widthB &&
                        0 <= yB && yB < heightB)
                    {
                        // Get the colors of the overlapping pixels
                        Color color = colorData[(rectangleA.X + xA) + (rectangleA.Y + yA) * strideA];
                        CollisionInfo info = collisionData[xB + yB * widthB];

                        // If both pixels are not completely transparent,
                        if (color.A != 0 && info.type != CollisionInfo.CollisionType.Empty)
                        {
                            collisions.Add(info);
                        }
                    }

                    // Move to the next pixel in the row
                    posInB += stepX;
                }

                // Move to the next row
                yPosInB += stepY;
            }

            if (collisions.Count > 0)
                return true;

            // No intersection found
            return false;
        }

    }
}